// 版权所有2009 Go作者。保留所有权利。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

// 包打印机实现AST节点的打印。
package printer

import (
	"fmt"
	"go/ast"
	"go/build/constraint"
	"go/token"
	"io"
	"os"
	"strings"
	"text/tabwriter"
	"unicode"
)

const (
	maxNewlines = 2     // 源文本之间的最大换行数
	debug       = false // 启用调试
	infinity    = 1 << 30
)

type whiteSpace byte

const (
	ignore   = whiteSpace(0)
	blank    = whiteSpace(' ')
	vtab     = whiteSpace('\v')
	newline  = whiteSpace('\n')
	formfeed = whiteSpace('\f')
	indent   = whiteSpace('>')
	unindent = whiteSpace('<')
)

// 一个pmode值表示当前打印机模式。
type pmode int

const (
	noExtraBlank     pmode = 1 << iota // 在/*-样式注释
	noExtraLinebreak                   // 在/*-样式注释
)

type commentInfo struct {
	cindex         int               // 当前注释索引
	comment        *ast.CommentGroup // /=打印机后禁用额外换行符。评论[cindex]；或无
	commentOffset  int               // /=打印机。posFor（printer.comments[cindex].List[0].Pos（））。抵消或者如果注释组包含换行符，则为无穷大
	commentNewline bool              // true 
}

type printer struct {
	// 配置（初始化后不变）
	Config
	fset *token.FileSet

	// 当前状态
	output       []byte       // 原始打印机结果
	indent       int          // 当前缩进
	level        int          // 级别==0:外部复合文字；级别>0：内部复合文字
	mode         pmode        // 当前打印机模式
	endAlignment bool         // 如果设置，立即终止对齐
	impliedSemi  bool         // 如果设置，换行符表示分号
	lastTok      token.Token  // 最后一个打印的标记（如果是空白，则为token.invalize）
	prevOpen     token.Token  // 之前的非大括号“open”标记（，[，或token.unliked 
	wsbuf        []whiteSpace // 延迟的空格
	goBuild      []int        // 所有
	plusBuild    []int        // 开始索引所有

	// 位置
	// 当结果
	// 格式与源格式不同时，out位置与pos位置不同（以
	// 空格的数量计）.如果存在差异且SourcePos设置为
	// 配置模式，则
	// 读卡器的原始源位置。
	pos     token.Position // 在AST（源）空间中的当前位置
	out     token.Position // 在输出空间中的当前位置
	last    token.Position // 调用writeString后的pos值
	linePtr *int           // 如果设置，则记录出来。*linePtr 

	// 所有源注释列表中下一个标记的行，按出现顺序排列。
	comments        []*ast.CommentGroup // 可能为零
	useNodeComments bool                // 如果未设置，则忽略节点的引线注释

	// 关于p.comments[p.cindex]的信息；由nextComment设置。
	commentInfo

	// 已计算节点大小的缓存。
	nodeSizes map[ast.Node]int

	// 最近计算的行位置的缓存。
	cachedPos  token.Pos
	cachedLine int // 对应于cachedPos的行
}

func (p *printer) init(cfg *Config, fset *token.FileSet, nodeSizes map[ast.Node]int) {
	p.Config = *cfg
	p.fset = fset
	p.pos = token.Position{Line: 1, Column: 1}
	p.out = token.Position{Line: 1, Column: 1}
	p.wsbuf = make([]whiteSpace, 0, 16) // 空格序列是短的
	p.nodeSizes = nodeSizes
	p.cachedPos = -1
}

func (p *printer) internalError(msg ...any) {
	if debug {
		fmt.Print(p.pos.String() + ": ")
		fmt.Println(msg...)
		panic("go/printer")
	}
}

// commentsHaveNewline报告是否属于
// an*ast的注释列表。CommentGroup包含换行符。由于位置信息
// 可能仅部分正确，我们还必须阅读注释文本。
func (p *printer) commentsHaveNewline(list []*ast.Comment) bool {
	// len（list）>0 
	line := p.lineFor(list[0].Pos())
	for i, c := range list {
		if i > 0 && p.lineFor(list[i].Pos()) != line {
			// 不是所有评论都在同一行
			return true
		}
		if t := c.Text; len(t) >= 2 && (t[1] == '/' || strings.Contains(t, "\n")) {
			return true
		}
	}
	_ = line
	return false
}

func (p *printer) nextComment() {
	for p.cindex < len(p.comments) {
		c := p.comments[p.cindex]
		p.cindex++
		if list := c.List; len(list) > 0 {
			p.comment = c
			p.commentOffset = p.posFor(list[0].Pos()).Offset
			p.commentNewline = p.commentsHaveNewline(list)
			return
		}
		// 我们不应该到达这里（正确的ast没有空的
		// ast CommentGroup节点），但是要保守，再试一次
	}
	// 没有更多注释
	p.commentOffset = infinity
}

// commentBefore报告当前注释组是否出现
// 在源代码和打印的下一个位置之前，它没有引入隐式分号。
// 
func (p *printer) commentBefore(next token.Position) bool {
	return p.commentOffset < next.Offset && (!p.impliedSemi || !p.commentNewline)
}

// commentSizeBefore返回下一个位置前同一行上的
// 注释的估计大小。
// 
func (p *printer) commentSizeBefore(next token.Position) int {
	// 保存/恢复当前的p.commentInfo（p.nextComment（）修改它）
	defer func(info commentInfo) {
		p.commentInfo = info
	}(p.commentInfo)

	size := 0
	for p.commentBefore(next) {
		for _, c := range p.comment.List {
			size += len(c.Text)
		}
		p.nextComment()
	}
	return size
}

// 记录行记录下一个非空白的输出行号
// 在*linePtr中标记。它用于计算
// 格式化构造的准确行号，与挂起（尚未发出）空格
// 或注释无关。
// 
func (p *printer) recordLine(linePtr *int) {
	p.linePtr = linePtr
}

// linesFrom返回当前
// 输出行和line参数之间的输出行数，忽略任何挂起（尚未发出）的空白或注释。它用于计算格式化构造的精确
// 大小（以行数为单位）。
// 
func (p *printer) linesFrom(line int) int {
	return p.out.Line - line
}

func (p *printer) posFor(pos token.Pos) token.Position {
	// 使用频率不足以缓存整个令牌。位置
	return p.fset.PositionFor(pos, false /* absolute position */)
}

func (p *printer) lineFor(pos token.Pos) int {
	if pos != p.cachedPos {
		p.cachedPos = pos
		p.cachedLine = p.fset.PositionFor(pos, false /* absolute position */).Line
	}
	return p.cachedLine
}

// writeLineDirective在必要时写入
func (p *printer) writeLineDirective(pos token.Position) {
	if pos.IsValid() && (p.out.Line != pos.Line || p.out.Filename != pos.Filename) {
		p.output = append(p.output, tabwriter.Escape) // 保护
		p.output = append(p.output, fmt.Sprintf("// 行%s:%d\n”，位置文件名，位置行）…）
		p.output = append(p.output, tabwriter.Escape)
		// p.out必须与
		p.out.Filename = pos.Filename
		p.out.Line = pos.Line
	}
}

// 写独立写入缩进匹配。
func (p *printer) writeIndent() {
	// 使用“硬“htabs-缩进列
	// 不能被tabwriter 
	n := p.Config.Indent + p.indent // 包括基本缩进
	for i := 0; i < n; i++ {
		p.output = append(p.output, '\t')
	}

	// 更新位置
	p.pos.Offset += n
	p.pos.Column += n
	p.out.Column += n
}

// writeByte将chn次写入p.output并更新p.pos.
// 仅用于写入格式化（空白）字符。
func (p *printer) writeByte(ch byte, n int) {
	if p.endAlignment {
		// 忽略任何对齐控制字符；
		// 在行尾，用
		// 一个formfeed分隔，以指示现有列的终止。
		switch ch {
		case '\t', '\v':
			ch = ' '
		case '\n', '\f':
			ch = '\f'
			p.endAlignment = false
		}
	}

	if p.out.Column == 1 {
		// 无需在空白前写入行指令
		p.writeIndent()
	}

	for i := 0; i < n; i++ {
		p.output = append(p.output, ch)
	}

	// 更新位置
	p.pos.Offset += n
	if ch == '\n' || ch == '\f' {
		p.pos.Line += n
		p.out.Line += n
		p.pos.Column = 1
		p.out.Column = 1
		return
	}
	p.pos.Column += n
	p.out.Column += n
}

// writeString将字符串s写入p.output并更新p.pos、p.out、
// 和p.last。如果设置了isLit，则s将通过tabwriter转义。转义字符
// 防止tabwriter解释s。
// 
// 注意：writeString仅用于编写Go标记、文字和
// 注释，所有这些注释都必须按字面意思编写。因此，总是将isLit设置为true是正确的。但是，仅当需要时（即，当我们不知道s不包含制表符或换行符时），才显式设置它可以避免处理额外的转义字符，并将
// 打印机基准的运行时间减少多达10%。
// 
func (p *printer) writeString(pos token.Position, s string, isLit bool) {
	if p.out.Column == 1 {
		if p.Config.Mode&SourcePos != 0 {
			p.writeLineDirective(pos)
		}
		p.writeIndent()
	}

	if pos.IsValid() {
		// 更新p.pos（如果pos无效，继续使用现有的p.pos）
		// 注意：必须在处理行开始后执行此操作，因为
		// 如果有缩进，writeIndent将更新p.pos，但是p.pos 
		// 是s.
		p.pos = pos
	}

	if isLit {
		// 保护通过tabwriter 
		// 的位置不变。请注意，有效的Go程序不能包含
		// tabwriter。转义字节，因为它们不出现在合法的
		// UTF-8序列中。
		p.output = append(p.output, tabwriter.Escape)
	}

	if debug {
		p.output = append(p.output, fmt.Sprintf("/*%s*/", pos)...) // 不要更新p.pos！
	}
	p.output = append(p.output, s...)

	// 更新位置
	nlines := 0
	var li int // 最后一行的索引；如果nlines>0 
	for i := 0; i < len(s); i++ {
		// 原始字符串文字可以包含除后引号（`）以外的任何字符，则此项有效。
		if ch := s[i]; ch == '\n' || ch == '\f' {
			// 解释换行
			nlines++
			li = i
			// 文本中的换行符将打断任何列
			// 格式设置到位；忽略通过
			// 线路末端的任何进一步对齐。
			p.endAlignment = true
		}
	}
	p.pos.Offset += len(s)
	if nlines > 0 {
		p.pos.Line += nlines
		p.out.Line += nlines
		c := len(s) - li
		p.pos.Column = c
		p.out.Column = c
	} else {
		p.pos.Column += len(s)
		p.out.Column += len(s)
	}

	if isLit {
		p.output = append(p.output, tabwriter.Escape)
	}

	p.last = p.pos
}

// writeCommentPrefix在注释前写入空格。
// 如果有任何悬而未决的空白，它会消耗尽可能多的
// 这可能有助于很好地定位评论。
// pos是注释位置，下一个是项目的位置
// 在所有待处理的注释之后，prev是
// 中的上一个注释组（或nil），tok是下一个标记。
// 
func (p *printer) writeCommentPrefix(pos, next token.Position, prev *ast.Comment, tok token.Token) {
	if len(p.output) == 0 {
		// 注释是第一个要打印的项目-不要在其他文件中写入任何空格
		return
	}

	if pos.IsValid() && pos.Filename != p.last.Filename {
		// 注释-用换行符分隔
		p.writeByte('\f', maxNewlines)
		return
	}

	if pos.Line == p.last.Line && (prev == nil || prev.Text[1] != '/') {
		// 注释与最后一个项目在同一行：
		// 用至少一个分隔符分隔
		hasSep := false
		if prev == nil {
			// 注释组的第一条注释
			j := 0
			for i, ch := range p.wsbuf {
				switch ch {
				case blank:
					// 忽略任何注释注释前的空格
					p.wsbuf[i] = ignore
					continue
				case vtab:
					// 尊重现有选项卡-重要
					// 对于注释结构的正确格式
					hasSep = true
					continue
				case indent:
					// 应用待定缩进
					continue
				}
				j = i
				break
			}
			p.writeWhitespace(j)
		}
		// 确保至少有一个分隔符
		if !hasSep {
			sep := byte('\t')
			if pos.Line == next.Line {
				// 下一项与注释
				// （必须是/*风格的注释）：分隔
				// 用空格代替制表符
				sep = ' '
			}
			p.writeByte(sep, 1)
		}

	} else {
		// 在另一行上进行注释：
		// 用至少一个换行符
		droppedLinebreak := false
		j := 0
		for i, ch := range p.wsbuf {
			switch ch {
			case blank, vtab:
				// 在换行前忽略任何水平空格
				p.wsbuf[i] = ignore
				continue
			case indent:
				// 应用待定缩进
				continue
			case unindent:
				// 如果这不是最后一个未登录项，应用它
				// 因为它（很可能）属于最后一个
				// 构造（例如，一个多行表达式列表）
				// 并且不是关闭一个块的一部分
				if i+1 < len(p.wsbuf) && p.wsbuf[i+1] == unindent {
					continue
				}
				// 如果下一个标记不是关闭标记}，那么如果注释似乎与
				// 标记；否则，假设未登录者是
				// 标记一致，则应用未登录的
				// /关闭块的一部分并停止（此场景显示
				// 注释位于案例标签之前，其中注释
				// 适用于下一个案例，而不是当前案例）
				if tok != token.RBRACE && pos.Column == next.Column {
					continue
				}
			case newline, formfeed:
				p.wsbuf[i] = ignore
				droppedLinebreak = prev == nil // 仅记录组的第一条注释
			}
			j = i
			break
		}
		p.writeWhitespace(j)

		// 确定注释之前的换行数
		n := 0
		if pos.IsValid() && p.last.IsValid() {
			n = pos.Line - p.last.Line
			if n < 0 { // 永远不应该发生
				n = 0
			}
		}

		// 仅在包范围级别（p.indent==0），
		// 如果我们之前删除了一行，请添加一个额外的换行符：
		// 这将在文档
		// 在包范围级别的注释（2570期）
		if p.indent == 0 && droppedLinebreak {
			n++
		}

		// 确保至少有一个换行符
		// 如果前一条注释是行注释
		if n == 0 && prev != nil && prev.Text[1] == '/' {
			n = 1
		}

		if n > 0 {
			// 使用formfeeds在注释前打断列；
			// 这类似于使用FormFeed来分隔
			// 独立的/*风格注释行
			p.writeByte('\f', nlimit(n))
		}
	}
}

// 如果s只包含空格
// （打印机上下文中只能显示制表符和空格），则返回true。
// 
func isBlank(s string) bool {
	for i := 0; i < len(s); i++ {
		if s[i] > ' ' {
			return false
		}
	}
	return true
}

// commonPrefix返回a和b的公共前缀。
func commonPrefix(a, b string) string {
	i := 0
	for i < len(a) && i < len(b) && a[i] == b[i] && (a[i] <= ' ' || a[i] == '*') {
		i++
	}
	return a[0:i]
}

// trimRight返回删除尾随空格的s。
func trimRight(s string) string {
	return strings.TrimRightFunc(s, unicode.IsSpace)
}

// stripCommonPrefix从
// 注释行，除第一行外，其他所有行都有某种形式的空格前缀）。
// 前缀是使用启发式算法计算出来的，这样一来，在使用打印机的
// 内容可能会被很好地排列。
// 当前缩进重新打印每一行之后，注释
// 
func stripCommonPrefix(lines []string) {
	if len(lines) <= 1 {
		return // 最多一行-无需做
	}
	// len（行）>1 

	// 此函数中的启发式函数尝试处理一些
	// 常见的/*-风格注释模式：
	// 开头/*和结尾*/对齐，
	// 其余注释文本对齐并对齐空格或制表符缩进，左边有垂直的“星线”空格或制表符，结尾*/与最后一条注释文本在同一行。

	// 计算除第一行、
	// 最后一行和空行以外的所有行的最大公共白色前缀，并将空行替换为空的
	// 行（第一行以/*开头，没有前缀）。ABCFDG 
	prefix := ""
	prefixSet := false
	if len(lines) > 2 {
		for i, line := range lines[1 : len(lines)-1] {
			if isBlank(line) {
			} else {
				if !prefixSet {
					prefix = line
					prefixSet = true
				}
				prefix = commonPrefix(prefix, line)
			}

		}
	}
	if !prefixSet {
		line := lines[len(lines)-1]
		prefix = commonPrefix(line, line)
	}

	/*
	 * Check for vertical "line of stars" and correct prefix accordingly.
	 */
	lineOfStars := false
	if p, _, ok := strings.Cut(prefix, "*"); ok {
		// 从前缀中删除尾随空格，使星星保持对齐
		prefix = strings.TrimSuffix(p, " ")
		lineOfStars = true
	} else {
		// 不存在星星行。
		// 确定
		// 之后第一行的空白，在注释文本开头之前，假设两个
		// 空格而不是
		// 后面的第一个字符是制表符。如果第一个注释行为空，但开头为
		// 则假设最多有3个空格或一个制表符。这个
		// 空白可以在公共前缀中作为后缀找到。
		first := lines[0]
		if isBlank(first[2:]) {
			// 第一行无注释文本：
			// 将前缀最多减少3个空格或一个制表符
			// 如果存在-这将使注释文本相对于
			// 如果缩进
			// 首先
			i := len(prefix)
			for n := 0; n < 3 && i > 0 && prefix[i-1] == ' '; n++ {
				i--
			}
			if i == len(prefix) && i > 0 && prefix[i-1] == '\t' {
				i--
			}
			prefix = prefix[0:i]
		} else {
			// 第一行的注释文本
			suffix := make([]byte, len(first))
			n := 2 // 打开后开始
			for n < len(first) && first[n] <= ' ' {
				suffix[n] = first[n]
				n++
			}
			if n > 2 && suffix[2] == '\t' {
				// 假设“\t”补偿了
				suffix = suffix[2:n]
			} else {
				// 否则假设两个空格
				suffix[0], suffix[1] = ' ', ' '
				suffix = suffix[0:n]
			}
			// 如果发现它是前缀的后缀，则将计算出的公共前缀缩短为
			// 后缀的长度。
			prefix = strings.TrimSuffix(prefix, string(suffix))
		}
	}

	// 句柄最后一行：如果它只包含一个结束*/，请将其与开始
	// 行对齐。
	last := lines[len(lines)-1]
	closing := "*/"
	before, _, _ := strings.Cut(last, closing) // 结束始终存在
	if isBlank(before) {
		// 最后一行仅包含结束*/
		if lineOfStars {
			closing = " */" // 添加空白以对齐最终星
		}
		lines[len(lines)-1] = prefix + closing
	} else {
		// 最后一行包含更多注释文本-假设
		// 它与其他行一样对齐，并在前缀计算中包含
		// 从除首先是空行。
		prefix = commonPrefix(prefix, last)
	}

	for i, line := range lines {
		if i > 0 && line != "" {
			lines[i] = line[len(prefix):]
		}
	}
}

func (p *printer) writeComment(comment *ast.Comment) {
	text := comment.Text
	pos := p.posFor(comment.Pos())

	const linePrefix = "// 线路“
	if strings.HasPrefix(text, linePrefix) && (!pos.IsValid() || pos.Column == 1) {
		// 可能是一个
		// 暂时暂停缩进以保持行指令有效。
		defer func(indent int) { p.indent = indent }(p.indent)
		p.indent = 0
	}

	// 
	if text[1] == '/' {
		if constraint.IsGoBuild(text) {
			p.goBuild = append(p.goBuild, len(p.output))
		} else if constraint.IsPlusBuild(text) {
			p.plusBuild = append(p.plusBuild, len(p.output))
		}
		p.writeString(pos, trimRight(text), true)
		return
	}

	// 的捷径常见案例
	lines := strings.Split(text, "\n")

	// 添加到所有行，使它们看起来像缩进
	// before-这将确保公共前缀计算
	// 与应用
	// 格式的次数无关（was问题1835）。
	if pos.IsValid() && pos.Column == 1 && p.indent > 0 {
		for i, line := range lines[1:] {
			lines[1+i] = "   " + line
		}
	}

	stripCommonPrefix(lines)

	// 写注释行，用formfeed分隔，
	// 最后一行后不带换行符
	for i, line := range lines {
		if i > 0 {
			p.writeByte('\f', 1)
			pos = p.pos
		}
		if len(line) > 0 {
			p.writeString(pos, trimRight(line), true)
		}
	}
}

// 如果指示
// 写注释后换行符，并处理任何剩余的缩进信息。如果需要换行符
// 则换行符的类型（换行符与formfeed）取决于
// 挂起的空格。writeCommentSuffix结果指示是否写入了
// 换行符，或者是否从空白
// 缓冲区删除了formfeed。
// 
func (p *printer) writeCommentSuffix(needsLinebreak bool) (wroteNewline, droppedFF bool) {
	for i, ch := range p.wsbuf {
		switch ch {
		case blank, vtab:
			// 忽略尾随空格
			p.wsbuf[i] = ignore
		case indent, unindent:
			// 不要丢失缩进信息
		case newline, formfeed:
			// 如果我们需要换行，只保留一个
			// 但请记住，如果我们删除了任何formfeeds 
			if needsLinebreak {
				needsLinebreak = false
				wroteNewline = true
			} else {
				if ch == formfeed {
					droppedFF = true
				}
				p.wsbuf[i] = ignore
			}
		}
	}
	p.writeWhitespace(len(p.wsbuf))

	// 请确保我们有一个换行符
	if needsLinebreak {
		p.writeByte('\n', 1)
		wroteNewline = true
	}

	return
}

// containsLinebreak报告空白缓冲区是否包含任何换行符。
func (p *printer) containsLinebreak() bool {
	for _, ch := range p.wsbuf {
		if ch == newline || ch == formfeed {
			return true
		}
	}
	return false
}

// intersperseComments会使用出现在下一个标记
// tok之前的所有注释，并将其与缓冲空白（即需要在下一个标记之前写入的空白
// 一起打印）。一种启发式方法用于混合
// 注释和空格。intersperseComments结果指示是否写入了
// 换行符，或者是否从空白缓冲区中删除了formfeed。
// 
func (p *printer) intersperseComments(next token.Position, tok token.Token) (wroteNewline, droppedFF bool) {
	var last *ast.Comment
	for p.commentBefore(next) {
		for _, c := range p.comment.List {
			p.writeCommentPrefix(p.posFor(c.Pos()), next, last, tok)
			p.writeComment(c)
			last = c
		}
		p.nextComment()
	}

	if last != nil {
		// 如果最后一条注释是/*风格的注释，下一项
		// 紧跟在同一行，但不是逗号，也不是“结束”
		// 标记，则在其相应的“开始”标记后面添加一个额外的分隔符，除非明确禁用。使用空白
		// 作为分隔符，除非我们有挂起的换行符，它们不是
		// 禁用的，并且我们在复合文字之外，在这种情况下
		// 我们需要换行符（15137期）。这已经变得过于复杂了。我们应该能够追踪我们是否在表达式或语句中，并使用这些信息更直接地做出决定。
		needsLinebreak := false
		if p.mode&noExtraBlank == 0 &&
			last.Text[1] == '*' && p.lineFor(last.Pos()) == next.Line &&
			tok != token.COMMA &&
			(tok != token.RPAREN || p.prevOpen == token.LPAREN) &&
			(tok != token.RBRACK || p.prevOpen == token.LBRACK) {
			if p.containsLinebreak() && p.mode&noExtraLinebreak == 0 && p.level == 0 {
				needsLinebreak = true
			} else {
				p.writeByte(' ', 1)
			}
		}
		// 除非明确禁用，否则确保在EOF之前和关闭“}”之前的
		// 之后有换行。
		if last.Text[1] == '/' ||
			tok == token.EOF ||
			tok == token.RBRACE && p.mode&noExtraLinebreak == 0 {
			needsLinebreak = true
		}
		return p.writeCommentSuffix(needsLinebreak)
	}

	// 未编写任何注释-我们不应该到达这里，因为
	// 在这种情况下不应调用intersperseComments 
	p.internalError("intersperseComments called without pending comments")
	return
}

// whiteWhitespace写入前n个空格条目。
func (p *printer) writeWhitespace(n int) {
	// 写入条目
	for i := 0; i < n; i++ {
		switch ch := p.wsbuf[i]; ch {
		case ignore:
			// 忽略！
		case indent:
			p.indent++
		case unindent:
			p.indent--
			if p.indent < 0 {
				p.internalError("negative indentation:", p.indent)
				p.indent = 0
			}
		case newline, formfeed:
			// 一个换行符紧接着一个“纠正”
			// 未登录项与未登录项交换-这允许
			// 正确的标签定位。如果注释在
			// 换行符和标签之间，则未注释不是
			// 注释空白前缀的一部分，注释
			// 将正确缩进。
			if i+1 < n && p.wsbuf[i+1] == unindent {
				// 使用formfeed终止当前节。
				// 否则，将
				// 引至宽列的下一行上的长标签名称可能会增加标签前行的缩进列
				// 数量；有效地导致错误的
				// 缩进。
				p.wsbuf[i], p.wsbuf[i+1] = unindent, formfeed
				i-- // 再做一次
				continue
			}
			fallthrough
		default:
			p.writeByte(byte(ch), 1)
		}
	}

	// 将剩余条目向下移动
	l := copy(p.wsbuf, p.wsbuf[n:])
	p.wsbuf = p.wsbuf[:l]
}

// ----------------------------------------------------------------------------------------
// 打印接口

// n限制为最大换行。
func nlimit(n int) int {
	if n > maxNewlines {
		n = maxNewlines
	}
	return n
}

func mayCombine(prev token.Token, next byte) (b bool) {
	switch prev {
	case token.INT:
		b = next == '.' // 1。
	case token.ADD:
		b = next == '+' // /++
	case token.SUB:
		b = next == '-' // /--
	case token.QUO:
		b = next == '*' // /*
	case token.LSS:
		b = next == '-' || next == '<' // /<-或<<
	case token.AND:
		b = next == '&' || next == '^' // /&或&^ 
	}
	return
}

// 打印“项目”列表（大致对应于语法
// /标记，但也包括空格和格式信息）。
// 这是唯一一个应该直接从
// 节点中的任何AST打印函数调用的打印函数。去
// 
// 累积空白，直到出现非空白标记。任何需要在该令牌打印之前出现的注释，都要考虑到任何待处理的白色的数量和结构，以获得最佳注释位置。然后，任何剩余的空格都会被
// 打印出来，后跟实际的标记。
// 
func (p *printer) print(args ...any) {
	for _, arg := range args {
		// 有关当前参数的信息
		var data string
		var isLit bool
		var impliedSemi bool // 此参数

		// 记录之前的开始标记，如果有任何
		switch p.lastTok {
		case token.ILLEGAL:
			// 忽略（空白）
		case token.LPAREN, token.LBRACK:
			p.prevOpen = p.lastTok
		default:
			// 其他标记跟随任何开始标记
			p.prevOpen = token.ILLEGAL
		}

		switch x := arg.(type) {
		case pmode:
			// 切换打印机模式
			p.mode ^= x
			continue

		case whiteSpace:
			if x == ignore {
				// 不要将忽略添加到缓冲区；他们可能会错误地“纠正”未登录者（参见
				// LabeledStmt）
				continue
			}
			i := len(p.wsbuf)
			if i == cap(p.wsbuf) {
				// 空格序列非常短，所以这应该不会发生。如果真的发生了，请优雅地处理（但也可能使用
				// 错误的评论位置）。
				p.writeWhitespace(i)
				i = 0
			}
			p.wsbuf = p.wsbuf[0 : i+1]
			p.wsbuf[i] = x
			if x == newline || x == formfeed {
				// 换行符影响当前状态（p.impliedSemi）
				// 而不是打印arg后的状态（impliedSemi）
				// 因为注释可以散布在arg 
				// 在这种情况下
				p.impliedSemi = false
			}
			p.lastTok = token.ILLEGAL
			continue

		case *ast.Ident:
			data = x.Name
			impliedSemi = true
			p.lastTok = token.IDENT

		case *ast.BasicLit:
			data = x.Value
			isLit = true
			impliedSemi = true
			p.lastTok = x.Kind

		case token.Token:
			s := x.String()
			if mayCombine(p.lastTok, s[0]) {
				// 上一个和当前标记必须是
				// 用空格分隔，否则它们将合并
				// 进入另一个不正确的令牌序列
				// （除了token.INT后跟“.”这个
				// 不应该发生，因为它是通过二进制表达式格式处理
				// 的）
				if len(p.wsbuf) != 0 {
					p.internalError("whitespace buffer not empty")
				}
				p.wsbuf = p.wsbuf[0:1]
				p.wsbuf[0] = ' '
			}
			data = s
			// 一些关键字后跟换行符意味着分号
			switch x {
			case token.BREAK, token.CONTINUE, token.FALLTHROUGH, token.RETURN,
				token.INC, token.DEC, token.RPAREN, token.RBRACK, token.RBRACE:
				impliedSemi = true
			}
			p.lastTok = x

		case token.Pos:
			if x.IsValid() {
				p.pos = p.posFor(x) // 下一项的准确位置
			}
			continue

		case string:
			// AST不正确-打印错误消息
			data = x
			isLit = true
			impliedSemi = true
			p.lastTok = token.STRING

		default:
			fmt.Fprintf(os.Stderr, "print: unsupported argument %v (%T)\n", arg, arg)
			panic("go/printer type")
		}
		// 数据！=“

		next := p.pos // 下一项的估计/准确位置
		wroteNewline, droppedFF := p.flush(next, p.lastTok)

		// 如果源代码和
		// 如果它们没有引起额外分号（不要在
		// flush中这样做，因为它会在文件末尾引起额外的换行）
		if !p.impliedSemi {
			n := nlimit(next.Line - p.pos.Line)
			// 如果我们已经写了一个
			if wroteNewline && n == maxNewlines {
				n = maxNewlines - 1
			}
			if n > 0 {
				ch := byte('\n')
				if droppedFF {
					ch = '\f' // /请不要超过最大换行数。”使用formfeed，因为我们在
				}
				p.writeByte(ch, n)
				impliedSemi = false
			}
		}

		// 下一个令牌现在开始-如果请求，记录其行号
		if p.linePtr != nil {
			*p.linePtr = p.out.Line
			p.linePtr = nil
		}

		p.writeString(next, data, isLit)
		p.impliedSemi = impliedSemi
	}
}

// flush打印文本
// 在下一个令牌tok的位置之前出现的任何待定注释和空白。如果写了换行符或从空白
// 。
// 缓冲区中删除了formfeed，则刷新结果将指示
// 
func (p *printer) flush(next token.Position, tok token.Token) (wroteNewline, droppedFF bool) {
	if p.commentBefore(next) {
		// 如果在下一项之前有注释，则将其散布
		wroteNewline, droppedFF = p.intersperseComments(next, tok)
	} else {
		// 否则，写入任何剩余的空格
		p.writeWhitespace(len(p.wsbuf))
	}
	return
}

// getNode返回ast。与n关联的CommentGroup（如果有）。
func getDoc(n ast.Node) *ast.CommentGroup {
	switch n := n.(type) {
	case *ast.Field:
		return n.Doc
	case *ast.ImportSpec:
		return n.Doc
	case *ast.ValueSpec:
		return n.Doc
	case *ast.TypeSpec:
		return n.Doc
	case *ast.GenDecl:
		return n.Doc
	case *ast.FuncDecl:
		return n.Doc
	case *ast.File:
		return n.Doc
	}
	return nil
}

func getLastComment(n ast.Node) *ast.CommentGroup {
	switch n := n.(type) {
	case *ast.Field:
		return n.Comment
	case *ast.ImportSpec:
		return n.Comment
	case *ast.ValueSpec:
		return n.Comment
	case *ast.TypeSpec:
		return n.Comment
	case *ast.GenDecl:
		if len(n.Specs) > 0 {
			return getLastComment(n.Specs[len(n.Specs)-1])
		}
	case *ast.File:
		if len(n.Comments) > 0 {
			return n.Comments[len(n.Comments)-1]
		}
	}
	return nil
}

func (p *printer) printNode(node any) error {
	// 解包*注释节点，如果有
	var comments []*ast.CommentGroup
	if cnode, ok := node.(*CommentedNode); ok {
		node = cnode.Node
		comments = cnode.Comments
	}

	if comments != nil {
		// 注释节点-将注释列表限制在相关范围
		n, ok := node.(ast.Node)
		if !ok {
			goto unsupported
		}
		beg := n.Pos()
		end := n.End()
		// 如果节点有相关文档，
		// 将该评论组包含在范围
		// （注释列表按照源代码中注释外观的顺序进行排序）
		if doc := getDoc(n); doc != nil {
			beg = doc.Pos()
		}
		if com := getLastComment(n); com != nil {
			if e := com.End(); e > end {
				end = e
			}
		}
		// 标记。Pos值是全局偏移量，我们可以直接比较它们使用ast。文件注释，如果有任何
		i := 0
		for i < len(comments) && comments[i].End() < beg {
			i++
		}
		j := i
		for j < len(comments) && comments[j].Pos() < end {
			j++
		}
		if i < j {
			p.comments = comments[i:j]
		}
	} else if n, ok := node.(*ast.File); ok {
		p.comments = n.Comments
	}

	// 如果没有注释，请使用节点注释
	p.useNodeComments = p.comments == nil

	// 准备好注释
	p.nextComment()

	p.print(pmode(0))

	// 格式化节点
	switch n := node.(type) {
	case ast.Expr:
		p.expr(n)
	case ast.Stmt:
		// 带标签的语句将取消缩进以定位标签。
		// 将p.indent设置为1，这样我们就不会得到缩进“下溢”。
		if _, ok := n.(*ast.LabeledStmt); ok {
			p.indent = 1
		}
		p.stmt(n, false)
	case ast.Decl:
		p.decl(n)
	case ast.Spec:
		p.spec(n, 1, false)
	case []ast.Stmt:
		// 带标签的语句将取消缩进以定位标签。
		// 将p.indent设置为1，这样我们就不会得到缩进“下溢”。
		for _, s := range n {
			if _, ok := s.(*ast.LabeledStmt); ok {
				p.indent = 1
			}
		}
		p.stmtList(n, 0, false)
	case []ast.Decl:
		p.declList(n)
	case *ast.File:
		p.file(n)
	default:
		goto unsupported
	}

	return nil

unsupported:
	return fmt.Errorf("go/printer: unsupported node type %T", node)
}

// ----------------------------------------------------------------------------------------
// 修剪器

// 修剪器是一个io。用于删除tabwriter的Writer筛选器。转义
// 字符、尾随空格和制表符，以及将formfeed 
// 和vtab字符转换为换行符和HTAB（如果未使用tabwriter 
// 的话）。由tabwriter括起的文本。转义字符通过
// 进行传递，保持不变。
// 
type trimmer struct {
	output io.Writer
	state  int
	space  []byte
}

// trimmer作为状态机实现。
// 它可以处于以下状态之一：
const (
	inSpace  = iota // 内部空间
	inEscape        // 由tabwriter括起来的内部文本。转义
	inText          // 内部文本
)

func (p *trimmer) resetSpace() {
	p.state = inSpace
	p.space = p.space[0:0]
}

// 设计说明：消除
// 函数中出现的额外空格很有诱惑力，因为它可以简化节点打印函数中的一些空格逻辑。
// 然而，这会打乱tabwriter的任何格式设置。

var aNewline = []byte("\n")

func (p *trimmer) Write(data []byte) (n int, err error) {
	// 不变量：
	// p.state==inSpace:
	// p.space未写
	// p.state==inEscape，inText:
	// 数据[m:n]未写
	m := 0
	var b byte
	for n, b = range data {
		if b == '\v' {
			b = '\t' // 转换为htab 
		}
		switch p.state {
		case inSpace:
			switch b {
			case '\t', ' ':
				p.space = append(p.space, b)
			case '\n', '\f':
				p.resetSpace() // 放弃尾随空间
				_, err = p.output.Write(aNewline)
			case tabwriter.Escape:
				_, err = p.output.Write(p.space)
				p.state = inEscape
				m = n + 1 // /+1：跳过tabwriter。退出
			default:
				_, err = p.output.Write(p.space)
				p.state = inText
				m = n
			}
		case inEscape:
			if b == tabwriter.Escape {
				_, err = p.output.Write(data[m:n])
				p.resetSpace()
			}
		case inText:
			switch b {
			case '\t', ' ':
				_, err = p.output.Write(data[m:n])
				p.resetSpace()
				p.space = append(p.space, b)
			case '\n', '\f':
				_, err = p.output.Write(data[m:n])
				p.resetSpace()
				if err == nil {
					_, err = p.output.Write(aNewline)
				}
			case tabwriter.Escape:
				_, err = p.output.Write(data[m:n])
				p.state = inEscape
				m = n + 1 // /+1：跳过tabwriter。转义
			}
		default:
			panic("unreachable")
		}
		if err != nil {
			return
		}
	}
	n = len(data)

	switch p.state {
	case inEscape, inText:
		_, err = p.output.Write(data[m:n])
		p.resetSpace()
	}

	return
}

// ---------------------------------------------------------------
// 公共接口

// 模式值是一组标志（或0）。他们控制印刷。
type Mode uint

const (
	RawFormat Mode = 1 << iota // 不要使用tabwriter；如果设置，UseSpace将被忽略
	TabIndent                  // 使用独立于UseSpace的制表符进行缩进
	UseSpaces                  // 使用空格而不是制表符进行对齐
	SourcePos                  // 发出
)

// 打印机的公共API中不包括以下模式，因为
// 编辑代码文本被视为超出范围。由于该模式为
// 未报告，因此也可以根据
// go/format和cmd/gofmt不断变化的需求修改或删除该模式，而不破坏
// 用户。见CL 240683中的讨论。
const (
	// normalizeEnumbers是指在打印时规范化数字
	// 文字前缀和指数。
	// 
	// 此值在go/format和cmd/gofmt中已知并由其使用。
	// 目前，对于那些
	// 软件包来说，在打印过程中应用数字规范化，而不是通过提前修改AST，更方便、更高效。
	normalizeNumbers Mode = 1 << 30
)

// 配置节点控制Fprint的输出。
type Config struct {
	Mode     Mode // 默认值：0 
	Tabwidth int  // 默认值：8 
	Indent   int  // 默认值：0（所有代码至少缩进了这么多）
}

// fprint实现fprint，并获取用于设置打印机状态的节点访问映射。
func (cfg *Config) fprint(output io.Writer, fset *token.FileSet, node any, nodeSizes map[ast.Node]int) (err error) {
	// 打印节点
	var p printer
	p.init(cfg, fset, nodeSizes)
	if err = p.printNode(node); err != nil {
		return
	}
	// 打印未完成的评论
	p.impliedSemi = false // EOF就像换行符
	p.flush(token.Position{Offset: infinity, Line: infinity}, token.EOF)

	// 输出现在被缓冲在p.output中。
	// 修复
	p.fixGoBuildLines()

	// 通过微调器重定向输出，以消除尾随空白
	// （必须取消对tabwriter的输入，因为尾随选项卡提供了
	// 格式信息。tabwriter可以提供微调
	// 功能，但在设置RawFormat时不使用tabwriter。）
	output = &trimmer{output: output}

	// 必要时通过tabwriter重定向输出
	if cfg.Mode&RawFormat == 0 {
		minwidth := cfg.Tabwidth

		padchar := byte('\t')
		if cfg.Mode&UseSpaces != 0 {
			padchar = ' '
		}

		twmode := tabwriter.DiscardEmptyColumns
		if cfg.Mode&TabIndent != 0 {
			minwidth = 0
			twmode |= tabwriter.TabIndent
		}

		output = tabwriter.NewWriter(output, minwidth, cfg.Tabwidth, 1, padchar, twmode)
	}

	// 通过tabwriter/trimmer写入打印机结果以输出
	if _, err = output.Write(p.output); err != nil {
		return
	}

	// 刷新tabwriter，如果有
	if tw, _ := output.(*tabwriter.Writer); tw != nil {
		err = tw.Flush()
	}

	return
}

// 注释节点绑定AST节点和相应注释。
// 它可以作为任何Fprint函数的参数提供。
// 
type CommentedNode struct {
	Node     any // ast。文件，或ast。Expr，ast。十二月，阿斯特。Spec或ast。Stmt 
	Comments []*ast.CommentGroup
}

// Fprint“pretty prints”为给定配置输出AST节点。
// 位置信息相对于文件集fset进行解释。
// 节点类型必须为*ast。文件，*CommentedNode，[]ast。12月[]日。Stmt、
// 或与ast兼容的赋值。Expr，ast。十二月，阿斯特。Spec或ast。Stmt。
// 
func (cfg *Config) Fprint(output io.Writer, fset *token.FileSet, node any) error {
	return cfg.fprint(output, fset, node, make(map[ast.Node]int))
}

// Fprint“pretty prints”要输出的AST节点。
// 它调用Config。使用默认设置打印。
// 请注意，gofmt使用制表符进行缩进，但使用空格进行对齐；
// 使用格式。用于匹配gofmt的输出的节点（包go/格式）。
// 
func Fprint(output io.Writer, fset *token.FileSet, node any) error {
	return (&Config{Tabwidth: 8}).Fprint(output, fset, node)
}
